Lecture 8¶
We'll do some more with classes.
References:
- The Sage Tutorial on objects and classes
- Chapter 11 of Mohit, and Bhaskar N. Das. Learn Python in 7 Days : Learn Efficient Python Coding Within 7 Days, Packt Publishing, Limited, 2017.
- The Python Tutorial: Chapter 9: Classes
An empty class¶
To think a bit more about classes, maybe it is useful to play around with the simplest class:
class EmptyClass:
pass
The pass
statement is used to express an empty code block. We've declared an EmptyClass
, but there is nothing in it.
We can construct an instance of this class:
e = EmptyClass()
When e
is printed, it indicates it is an EmptyClass
object and has a memory location assigned to it. We can use this memory to store values, similar to a dictionary. For example:
e
<__main__.EmptyClass object at 0x7f19e1387250>
e.a = sqrt(2)
e.a
sqrt(2)
We could even store a function in it. Such as:
def double(x):
return 2*x
e.d = double
e.d(4)
8
Instances of the class are individual objects. Typically you'd want them to have common features. The way I've been doing things above does not ensure this however. For example, if we construct another instance, it has no access to e.a
or e.d
:
e2 = EmptyClass()
e2.a
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[17], line 1 ----> 1 e2.a AttributeError: 'EmptyClass' object has no attribute 'a'
e2.d(5)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[18], line 1 ----> 1 e2.d(Integer(5)) AttributeError: 'EmptyClass' object has no attribute 'd'
Despite that an empty class is empty, there are a lot of items in it. The dir
function lists the attributes of an object. (The list dir(obj)
things that can be obtained/done with the object e.g., obj.add(obj2)
.) Here's what we get when we do dir(EmptyClass)
:
dir(e)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'd']
dir(EmptyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
There are a lot of commands above, all surrounded by two underscores. Our class is not really empty, these functions have been provided with default values. Here is some of their meanings:
__init__
: For setting variable values in new instances. This process is called initialization.__repr__
: Returns a string representation of the object intended for the programmer (e.g, for debugging).__str__
: Returns a string representation of the object intended to be easier to read than__repr__
.__eq__
: For checking for equality of objects.__hash__
: Allows the object to define a hash (for use in dictionaries and other data structures).
This seems good for now. These are probably the most common functions you would want to override. Overriding means we'll replace their default values with something more useful to us. The first function you'd want to override is __init__
, so you can construct objects in a class in a more uniform way than the above.
The projective plane¶
We can define the projective plane over any field, $F$. (More generally, you can define the projective plane over any division ring, but we'll focus on Fields.) The projective plane consists of equivalence classes of non-zero vectors in $F^3$. Two non-zero elements $x=(x_1, x_2, x_3)$ and $y=(y_1, y_2, y_3)$ are equivalent if there is a $k \in F$ such that $kx=y$, where $k$ is acting by scalar multiplication.
These equivalence classes of vectors are known as points. The equivalence class makes up a line through the origin $(0,0,0)$ with the origin removed. Points are organized in lines. Our goal is to write three classes:
ProjectivePlane
: This object represents the full plane. It is important that it will keep track of the field. (There are multiple projective planes, one for each field.)ProjectivePoint
: Which will represent a point in a projective plane. We started work on this last week.ProjectiveLine
: Which will represent a line in a projective plane.
We want to be able to do things like this:
- Given two points, use
point1.join(point2)
to construct the line through the two points. - Given two lines, use
line1.intersect(line2)
to construct the intersection. - Print points and lines in a meaningful way.
- Test these objects for equality.
- Plot points and lines.
- Use them as keys in a dictionary (so for example we could store what color we'd like a line to be).
Overriding the __init__
method¶
The __init__
method has the following form:
def __init__(self, parameters...):
# Commands to initialize self
The self object is provided by Python: It is an empty object. The programmers job is to add attributes to this object.
Our ProjectivePlanes
are determined by a field. Here we write an __init__
method that has one parameter. We store this parameter in the self
object using the name _field
. The underscore in the variable name indicates (by convention) that the value should not typically be directly accessed by the user, and instead is for internal use of the class/object.
class ProjectivePlane:
def __init__(self, field):
self._field = field
Now to construct a ProjectivePlane
we must pass a field. Again, Python provides self
to the __init__
method. We don't call __init__
directly here. Instead we use ProjectivePlane(field)
. For example:
plane = ProjectivePlane(QQ)
plane
<__main__.ProjectivePlane object at 0x7f1989a15a90>
plane._field
Rational Field
Above we can see that RP2
is a ProjectivePlane
and its memory location. We can access the “internal” variable _field
if we wish:
Another example:
plane2 = ProjectivePlane(AA)
plane2
<__main__.ProjectivePlane object at 0x7f1988931f10>
plane2._field
Algebraic Real Field
plane3 = ProjectivePlane(pi)
plane3._field
pi
QQ.is_field()
True
pi.is_field()
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[33], line 1 ----> 1 pi.is_field() File ~/Git/sagemath/sage/src/sage/structure/element.pyx:495, in sage.structure.element.Element.__getattr__() 493 AttributeError: 'LeftZeroSemigroup_with_category.element_class' object has no attribute 'blah_blah'... 494 """ --> 495 return self.getattr_from_category(name) 496 497 cdef getattr_from_category(self, name): File ~/Git/sagemath/sage/src/sage/structure/element.pyx:508, in sage.structure.element.Element.getattr_from_category() 506 else: 507 cls = P._abstract_element_class --> 508 return getattr_from_other_class(self, cls, name) 509 510 def __dir__(self): File ~/Git/sagemath/sage/src/sage/cpython/getattr.pyx:363, in sage.cpython.getattr.getattr_from_other_class() 361 dummy_error_message.cls = type(self) 362 dummy_error_message.name = name --> 363 raise AttributeError(dummy_error_message) 364 attribute = <object>attr 365 # Check for a descriptor (__get__ in Python) AttributeError: 'sage.symbolic.expression.Expression' object has no attribute 'is_field'
class ProjectivePlane:
def __init__(self, field):
assert field.is_field()
self._field = field
self._vector_space = VectorSpace(self._field, 3)
ProjectivePlane(sqrt(2))
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[37], line 1 ----> 1 ProjectivePlane(sqrt(Integer(2))) Cell In[35], line 4, in ProjectivePlane.__init__(self, field) 3 def __init__(self, field): ----> 4 assert field.is_field() 5 self._field = field 6 self._vector_space = VectorSpace(self._field, Integer(3)) File ~/Git/sagemath/sage/src/sage/structure/element.pyx:495, in sage.structure.element.Element.__getattr__() 493 AttributeError: 'LeftZeroSemigroup_with_category.element_class' object has no attribute 'blah_blah'... 494 """ --> 495 return self.getattr_from_category(name) 496 497 cdef getattr_from_category(self, name): File ~/Git/sagemath/sage/src/sage/structure/element.pyx:508, in sage.structure.element.Element.getattr_from_category() 506 else: 507 cls = P._abstract_element_class --> 508 return getattr_from_other_class(self, cls, name) 509 510 def __dir__(self): File ~/Git/sagemath/sage/src/sage/cpython/getattr.pyx:363, in sage.cpython.getattr.getattr_from_other_class() 361 dummy_error_message.cls = type(self) 362 dummy_error_message.name = name --> 363 raise AttributeError(dummy_error_message) 364 attribute = <object>attr 365 # Check for a descriptor (__get__ in Python) AttributeError: 'sage.symbolic.expression.Expression' object has no attribute 'is_field'
plane = ProjectivePlane(QQ)
plane._vector_space
Vector space of dimension 3 over Rational Field
Defining methods¶
A method is a function we associate to an object in a class. The first parameter should always be an object in the class, and by convention is called self
.
We said above that the _field
variable is for internal use, but the user might want to know what it is. So, we'll define a field
method that returns it. Here is the updated class:
class ProjectivePlane:
def __init__(self, field):
assert field.is_field()
self._field = field
self._vector_space = VectorSpace(self._field, 3)
def field(self):
'Return the base field F'
return self._field
def vector_space(self):
'Return the vector space F^3'
return self._vector_space
Since we changed the class we should construct a new member:
plane = ProjectivePlane(QQ)
plane.field()
Rational Field
ProjectivePlane.field(plane)
Rational Field
plane.field?
Signature: plane.field() Docstring: Return the base field F Init docstring: Initialize self. See help(type(self)) for accurate signature. File: /tmp/ipykernel_7235/2025174784.py Type: method
ProjectivePlane.field?
Signature: ProjectivePlane.field(self) Docstring: Return the base field F Init docstring: Initialize self. See help(type(self)) for accurate signature. File: /tmp/ipykernel_7235/2025174784.py Type: function
There are two ways to call the method field
. First we can use the class:
The more natural way to do this is to call rational_plane.field()
.
plane.vector_space()
Vector space of dimension 3 over Rational Field
plane.vector_space()([3, 4,5])
(3, 4, 5)
These two ways of calling the function do the same thing. Essentially the call rational_plane.field()
is converted into ProjectivePlane.field(rational_plane)
by taking the object before the period and adding it as the first argument to the field
function from the class of rational_plane
.
Adding a second method
Note that the projective plane with field $F$ is closely related to the vector space $F^3$. It is reasonable for the class to make available this vector space. We add a _vector_space
variable to our projective planes, and add a method to access it below:
Let's test it.
The more verbose way to call it:
Note that above, I included a string in the first line of the definition of vector_space
. This provides documentation for the method. For example:
plane
<__main__.ProjectivePlane object at 0x7f1986d26e50>
The string methods¶
Here we add the methods that display an object as a string. There are two of them, repr()
and str()
.
Before we improve our class, why don't we take a look at these functions. For example, we'll import the datetime
module from the Python libary and get the current time.
import datetime
x = datetime.datetime.now()
x
datetime.datetime(2024, 9, 25, 16, 36, 15, 726440)
The str
representation of now is supposed to be human readable:
str(x)
'2024-09-25 16:36:15.726440'
The repr
representation of now gives a way of reconstructing it:
repr(x)
'datetime.datetime(2024, 9, 25, 16, 36, 15, 726440)'
If we run datetime.datetime(2024, 9, 24, 19, 42, 0, 582632)
we get something equal to now:
y = datetime.datetime(2024, 9, 25, 16, 36, 15, 726440)
x == y
True
Note that if an object is left at the end of a code block in Jupyter, the string printed is obtained from repr()
:
x
datetime.datetime(2024, 9, 25, 16, 36, 15, 726440)
print(x)
2024-09-25 16:36:15.726440
On the other hand if you print an object, you get the string from str()
:
QQ
Rational Field
print(QQ)
Rational Field
Remark: In Python, the convention seems to be that repr()
should produce a string allowing the programmer to reproduce the object. In SageMath, the convention seems to be different, and most objects seem to produce the same thing when you apply str()
and when you apply repr()
. We'll try to follow Python's convention with repr()
.
We'll now add __repr__
and __str__
methods to our class. They just need to return string representations.
class ProjectivePlane:
def __init__(self, field):
assert field.is_field()
self._field = field
self._vector_space = VectorSpace(self._field, 3)
def field(self):
'Return the base field F'
return self._field
def vector_space(self):
'Return the vector space F^3'
return self._vector_space
def __str__(self):
return f'Projective plane over {self._field}'
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
Tests:
plane = ProjectivePlane(QQ)
repr(plane)
'ProjectivePlane(Rational Field)'
str(plane)
'Projective plane over Rational Field'
plane
ProjectivePlane(Rational Field)
print(plane)
Projective plane over Rational Field
f' Hi {plane}'
' Hi Projective plane over Rational Field'
Checking objects for equality.¶
plane = ProjectivePlane(QQ)
plane2 = ProjectivePlane(QQ)
plane == plane2
False
plane == plane
True
The __eq__
method tests two objects for equality. Given the statement a == b
, assuming a
is an object, it calls
a.__eq__(b)
An important issue is that the object b
might not have the same type as a
(i.e., they might be defined by different classes). In this case we would want to return False
. Our implementation of equals for ProjectivePlane
uses type
which returns the class of an object.
The equals method is described in greater detail in the Python reference.
class ProjectivePlane:
def __init__(self, field):
assert field.is_field()
self._field = field
self._vector_space = VectorSpace(self._field, 3)
def field(self):
'Return the base field F'
return self._field
def vector_space(self):
'Return the vector space F^3'
return self._vector_space
def __str__(self):
return f'Projective plane over {self._field}'
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __eq__(self, other):
if type(self) != type(other):
return False
return self._field == other.field()
plane = ProjectivePlane(QQ)
plane2 = ProjectivePlane(QQ)
plane == plane2
True
plane == pi
False
type(plane)
<class '__main__.ProjectivePlane'>
type(pi)
<class 'sage.symbolic.expression.Expression'>
plane = ProjectivePlane(QQ)
plane2 = ProjectivePlane(AA)
plane == plane2
False
The above is the same as calling either of the following two commands:
Some other cases:
Note that the not equal operator !=
is the logical negation of the equals operator:
Implementing a hash¶
We might like to implement a hash so that objects can be used as keys in a dictionary. Currently, we can not do this:
d = {1: 17}
d[plane] = 5
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[84], line 2 1 d = {Integer(1): Integer(17)} ----> 2 d[plane] = Integer(5) TypeError: unhashable type: 'ProjectivePlane'
The reason is that hash(rational_plane1)
is not defined:
hash(rational_plane1)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[85], line 1 ----> 1 hash(rational_plane1) NameError: name 'rational_plane1' is not defined
A hash needs to return an int
. It needs to satisfy the rule that obj1 == obj2
implies hash(obj1) == hash(obj1)
. Ideally you want also that obj1 != obj2
implies hash(obj1) != hash(obj1)
, but this is difficult to guarantee. (The hash outputs a Python int so there are only finitely many choices.)
A hash collision is when you have obj1 != obj2
implies hash(obj1) == hash(obj1)
. Hash colisions will make data structures like dictionaries run slower, so they are best avoided. You can make hash collisions rare if you define your hash somewhat randomly.
Fortunately, immutable Sage objects typically already have a hash. So do tuples and immutable objects built into python. I like to represent the data of my object in a tuple and then take the hash of the tuple.
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
def __eq__(self, other):
if type(self) != type(other):
return False
# Now we know the objects are both ProjectivePlanes
return self.field() == other.field()
def __hash__(self):
# Return the hash of the pair consisting of 'ProjectivePlane' and the field:
return hash( (ProjectivePlane, self._field()) )
rational_plane1 = ProjectivePlane(QQ)
hash(rational_plane1)
-2856910268466969804
rational_plane2 = ProjectivePlane(QQ)
hash(rational_plane2) == hash(rational_plane1)
True
rational_plane3 = ProjectivePlane(AA)
hash(rational_plane3) == hash(rational_plane1)
False
d = {}
d[rational_plane1] = 3
d[rational_plane2] = 4
d[rational_plane3] = 5
d
{ProjectivePlane(Rational Field): 4, ProjectivePlane(Algebraic Real Field): 5}
ProjectivePoint¶
Here is the ProjectivePoint
class from class last time:
class ProjectivePoint:
V = VectorSpace(QQ, 3)
def __init__(self, v):
v = ProjectivePoint.V(v)
assert v != ProjectivePoint.V.zero()
self.v = v
def x(self):
if self.v[2]!=0:
return self.v[0] / self.v[2]
return Infinity
def y(self):
if self.v[2]!=0:
return self.v[1] / self.v[2]
return Infinity
Let's change it to store a copy of a ProjectivePlane
. The ProjectivePlane
will provide our vector space. The following additional changes were made:
- Changed the name of the stored vector to
self._v
(to indicate thatv
is private). - Added some docmentation for methods.
- We now raise errors when bad parameters are provided. The new things here are:
- We use isinstance to check if the provided
plane
is aProjectivePlane
. - We raise a TypeError if the type of
plane
is incorrect. - We raise a ValueError if the vector passed is the zero vector.
- For more on the error types, see the Python documentation.
- We use isinstance to check if the provided
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self._v[2]!=0:
return self._v[0] / self._v[2]
return Infinity
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self._v[2]!=0:
return self._v[1] / self._v[2]
return Infinity
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point.x(), point.y()
(1/3, 2/3)
Adding is_infinite()
, vector()
, and plane()
.¶
The projective plane is an extension of the usual plane. We can think of the usual plane ${\mathbb R}^2$ as consisting of equivalence classes of points of the form $(x, y, 1)$. Every equivalence class is represented except those where the last coordinate of the vector is zero. Points where vectors in the equivalence class have the last coordinate zero are considered to be at infinity in the plane. This is useful as we can already see from the x()
and y()
methods. We add it below.
We also added a vector
method which returns the underlying vector. Since we don't want the user to be able to change it, we set the vector to immutable in the constructor.
Finally we will give a way for the class to return the projective plane that contains it.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 0])
point.is_infinite()
True
point.vector()
(1, 2, 0)
point.plane()
ProjectivePlane(Rational Field)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point.is_infinite()
False
point.vector()
(1, 2, 3)
point.plane()
ProjectivePlane(Rational Field)
Adding string methods¶
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 0])
point
ProjectivePoint(ProjectivePlane(Rational Field), (1, 2, 0))
print(point)
common point at infinity of lines of slope 2
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point
ProjectivePoint(ProjectivePlane(Rational Field), (1, 2, 3))
print(point)
(1/3, 2/3)
Implementing equals¶
point = ProjectivePoint(rational_plane, [1, 2, 3])
point2 = ProjectivePoint(rational_plane, [2, 4, 6])
print(f'{point} ?= {point2}')
(1/3, 2/3) ?= (1/3, 2/3)
point.vector() == point2.vector()
False
Recall the equivalence relation on $F^3 \setminus \{{\mathbf 0}\}$: We say $\mathbf x$ and $\mathbf y$ are equivalent if there is a non-zero $c \in F$ such that $c \mathbf x = \mathbf y$. So we need two ProjectivePoints to be equal if they satisfy this for their vectors. If $i$ is an index such that $x_i \neq 0$, then the ratio $c$ must have the form $\frac{y_i}{x_i}$ if they are to be equal.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
def __eq__(self, other):
if not isinstance(other, ProjectivePoint):
return False
if self._v[0] != 0:
ratio = other._v[0] / self._v[0]
elif self._v[1] != 0:
ratio = other._v[1] / self._v[1]
else:
# It is not allowed for all 3 entries to be zero
ratio = other._v[2] / self._v[2]
return ratio*self._v == other._v
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point2 = ProjectivePoint(rational_plane, [2, 4, 6])
point1 == point2
True
point1 = ProjectivePoint(rational_plane, [0, 2, 3])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
point1 == point2
True
point1 = ProjectivePoint(rational_plane, [0, 2, 5])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
point1 == point2
False
point1 = ProjectivePoint(rational_plane, [0, 0, 5])
point2 = ProjectivePoint(rational_plane, [0, 0, 3/2])
point1 == point2
True
Implementing the hash¶
Recall the hash needs to satisfy point1 == point2
implies hash(point1) == hash(point2)
. To arrange this, we will product a “canonical representative” of the equivalence class represented by a point. Then we will hash this canonical representative.
We'll say a vector $v=(v_0, v_1, v_2)$ in $F^3$ is a canonical representative of its equivalence class if its first non-zero entry is one. Every equivalence class has exactly one canonical representative. For example, the canonical representative of the equivalence class of $(0, 7, -3)$ is $(0, 1, \frac{-3}{7})$.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
def __eq__(self, other):
if not isinstance(other, ProjectivePoint):
return False
if self._v[0] != 0:
ratio = other._v[0] / self._v[0]
elif self._v[1] != 0:
ratio = other._v[1] / self._v[1]
else:
# It is not allowed for all 3 entries to be zero
ratio = other._v[2] / self._v[2]
return ratio*self._v == other._v
def __hash__(self):
# cv will be the canonical representative of the equivalence class of self._v
if self._v[0] != 0:
cv = (1/self._v[0]) * self._v
elif self._v[1] != 0:
cv = (1/self._v[1]) * self._v
else:
cv = (1/self._v[2]) * self._v
# So we can hash cv, we set it to be immutable:
cv.set_immutable()
return hash((ProjectivePoint, cv))
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point2 = ProjectivePoint(rational_plane, [2, 4, 6])
hash(point1) == hash(point2)
True
point1 = ProjectivePoint(rational_plane, [0, 2, 3])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
hash(point1) == hash(point2)
True
point1 = ProjectivePoint(rational_plane, [0, 2, 5])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
hash(point1) == hash(point2)
False
point1 = ProjectivePoint(rational_plane, [0, 0, 5])
point2 = ProjectivePoint(rational_plane, [0, 0, 3/2])
hash(point1) == hash(point2)
True
Testing membership¶
Sage parents are often sets. Many Python objects are also containers (sets, tuples, dictionaries, lists, ...). Python has an in
keyword for testing membership. Examples below:
5 in [1, 2, 5]
True
3/2 in QQ
True
QQ.__contains__(3/2)
True
In our settings, we can consider a ProjectivePoint
to be in a ProjectivePlane
if the plane
passed to the point construtor equals the projective plane in question.
This particular case is not so interesting. But we plane to implement lines as well, and it is reasonable to ask if a point belongs to a line.
You can implement membership tests with the special __contains__
method. This would be defined in the container had have the form:
def __contains__(self, item):
# Return True if item is in self, False otherwise
So, a command such as x in S
gets converted to the method call S.__contains__(x)
. Let's update our ProjectivePlane
class:
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
def __eq__(self, other):
if type(self) != type(other):
return False
# Now we know the objects are both ProjectivePlanes
return self.field() == other.field()
def __hash__(self):
# Return the hash of the pair consisting of 'ProjectivePlane' and the field:
return hash( (ProjectivePlane, self._field()) )
def __contains__(self, item):
# Currently the only thing in a ProjectivePlane is a ProjectivePoint
# Later we will check other types (like Lines).
if isinstance(item, ProjectivePoint):
return self == item.plane()
return False
We test it:
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point1 in rational_plane
True
algebraic_plane = ProjectivePlane(AA)
point2 = ProjectivePoint(algebraic_plane, [1, 2, 3])
point2 in algebraic_plane
True
print(point2)
(1/3, 2/3)
point2 in rational_plane
False
point2.plane() == rational_plane
False
Remark. Sage gives the ability to do more general membership testing. For example points in ProjectivePlane(QQ)
would be contained in ProjectivePlane(AA)
since the rationals is a subfield of the Field of Algebraic real numbers, so a point in ProjectivePlane(QQ)
could be considered to be in
ProjectivePlane(AA)
. Sage handles these tests and conversions like this (from ProjectivePlane(QQ)
to ProjectivePlane(AA)
) via coercion. This is somewhat technical, but you can read about in the SageMath reference on Coercion. We might get to this later in the course, but for now we will content ourselves with classes defined at the Python level.
Anyway, currently the following returns False:
algebraic_plane = ProjectivePlane(QQ)
point1 in algebraic_plane
False